Python中容易混淆的知识点

函数

匿名函数

匿名函数就是不需要指定函数的名字,比如lambda x: x * x。相当与:

1
2
3
4
5
6
# ':'前的x为函数参数,后面的为返回值
def f(x):
return x * x
# 同样可以将匿名函数作为返回值返回
def b(x, y):
return lambda: x * x + y * y

函数参数

可变参数:可以接受所有参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def calc(*numbers):
sum = 0
for n in numbers:
sum = sum + n * n
return sum

>>> calc(1, 2, 3)
14


# 参数如果是list、tuple、可以直接在前面加 *
>>> nums = [1, 2, 3]
>>> calc(*nums)
14

关键字参数:使用**来定义,允许传入0个或者任意个含参数名的参数,在内部自动组装成dict

1
2
3
4
5
def person(name, age, **kw):
print('name:', name, 'age:', age, 'other:', kw)

>>> person('Jim', 18, city='Beijing')
name: Jim age: 18 other: {'city': 'Beijing'}

命名关键字参数:可以限制关键字参数的名字,比如只接受某个参数名为关键字参数。命名关键字参数需要用*进行分隔开,*后面的参数被视为关键字参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# 只接收city和job作为关键字参数。
def person(name, age, *, city, job):
print(name, age, city, job)

>>> person('Jack', 24, city='Beijing', job='Engineer')
Jack 24 Beijing Engineer

# 如果函数定义中已经有了一个可变参数,后面跟着的命名关键字参数就不再需要一个特殊分隔符'*'了:
def person(name, age, *args, city, job):
print(name, age, args, city, job)

命名关键字参数必须传入参数名,这和位置参数不同。如果没有传入参数名,调用将报错:

>>> person('Jack', 24, 'Beijing', 'Engineer')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: person() takes 2 positional arguments but 4 were given


# 命名关键字参数可以有缺省值,从而简化调用:
def person(name, age, *, city='Beijing', job):
print(name, age, city, job)

# 由于命名关键字参数city具有默认值,调用时,可不传入city参数:
>>> person('Jack', 24, job='Engineer')
Jack 24 Beijing Engineer


# 使用命名关键字参数时,要特别注意,如果没有可变参数,就必须加一个'*'作为特殊分隔符。如果缺少*,Python解释器将无法识别位置参数和命名关键字参数:
def person(name, age, city, job):
# 缺少 *,city和job被视为位置参数
pass

递归函数:

在函数内部,可以调用其他函数,如果一个函数在内部调用自身,那么就称作递归函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def fact(n):
if n==1:
return 1
return n * fact(n - 1)

===> fact(5)
===> 5 * fact(4)
===> 5 * (4 * fact(3))
===> 5 * (4 * (3 * fact(2)))
===> 5 * (4 * (3 * (2 * fact(1))))
===> 5 * (4 * (3 * (2 * 1)))
===> 5 * (4 * (3 * 2))
===> 5 * (4 * 6)
===> 5 * 24
===> 120

递归函数的优点:逻辑清晰,但是使用尾递归要注意防止栈溢出。(函数是通过栈这种数据结构来实现的。所以递归调用的次数越多,会导致栈溢出) 解决这个办法是通过尾递归优化。

尾递归优化:当函数返回的时候,调用自身本身,并且return语句不能包含表达式。这样编译器或者就是去就可以把尾递归做优化,使得尾递归不论本身调用多少次,都只占用了一个栈。不会出现栈溢出的情况。上面的例子return n * fact(n - 1)不是一个尾递归。

一个尾递归的例子:

1
2
3
4
5
6
7
8
# 这个函数是一个递归调用,所以并不会导致栈溢出。但是由于Python解释器并没有做尾递归优化,所以仍然会导致栈溢出。
def fact(n):
return fact_iter(n, 1)

def fact_iter(num, product):
if num == 1:
return product
return fact_iter(num - 1, num * product)

列表生成式:

如果想要生成一个list,可以使用list(range(1, 11)),也可使用for循环,然而使用列表生成式可以很方便的生成list:把要生成的元素x * x放到前面,后面跟着for循环,就可以把list创建出来

1
[x * x for x in range(1, 11)]

后面还可以跟if语句,求偶数的平方:

1
2
>>> [x * x for x in range(1, 11) if x % 2 == 0]
[4, 16, 36, 64, 100]

使用两层循环,使用两个变量来生成list:

1
2
3
4
5
6
>>> [m + n for m in 'ABC' for n in 'XYZ']
['AX', 'AY', 'AZ', 'BX', 'BY', 'BZ', 'CX', 'CY', 'CZ']

>>> d = {'x': 'A', 'y': 'B', 'z': 'C' }
>>> [k + '=' + v for k, v in d.items()]
['y=B', 'x=A', 'z=C']

生成器与迭代器

生成器:

Python中,一边循环一边计算的机制,称为生成器(generator),只要把一个列表生成式的[]改成(),就创建了generator。

有两种方式创建生成器:

1
2
3
4
5
6
7
8
9
# 列表生成式
[x * x for x in range(10)]
# generator
g =(x * x for x in range(10))

# 每次调用 next(g),就可以计算出g的下一个元素的值,直到最后抛出 StopIteration 错误。由于generator是可迭代对象。所以可以用for循环来遍历。
g = (x * x for x in range(10))
for n in g:
print(n)

第二种方法创建生成器,如果函数定义中包含yield关键字,那么这个函数就是一个生成器。在generator当中,每次调用next()的时候执行,遇到yield语句返回,再次执行时从上次返回的yield语句处继续执行,举个简单的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
def odd():
print('step 1')
yield 1
print('step 2')
yield(3)
print('step 3')
yield(5)

>>> o = odd()
>>> next(o)
step 1
1
>>> next(o)
step 2
3
>>> next(o)
step 3
5
>>> next(o)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration

在使用for循环调用generator时,如果想要拿到返回值,必须捕获错误,返回值包含在StopIterationvalue中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
>>> g = fib(6)
>>> while True:
... try:
... x = next(g)
... print('g:', x)
... except StopIteration as e:
... print('Generator return value:', e.value)
... break
...
g: 1
g: 1
g: 2
g: 3
g: 5
g: 8
Generator return value: done

###迭代器:

Python 中可以直接作用于for循环的对象统称为可迭代对象:Iterable。使用 isinstance() 判断一个对象是否是可迭代对象Iterable

1
2
3
4
from collections import Iterable
isinstance([], Iterable)
isinstance({}, Iterable)
isinstance(100, Iterable) // False

生成器不但可以作用于 for 循环,而且可以被 next() 函数不断调用返回下一个值,直到最后抛出StopIteration错误,这样可以被next()函数不断返回下一个值得对象称为迭代器:Iterator

生成器都是Iterator对象,但是listdictstr是可迭代对象(Iterable),但是不是迭代器Iterator。需要把以上类型变成可迭代对象可以使用 iter() 函数。

1
2
isinstance(iter([]), Iterator)
// True

listdictstr 等数据不是Iterator的原因是:Python 中的Iterator对象表示的是一个数据流,Iterator对象可以被next()函数不断调用返回下一个数据,直到没有数据时抛出StopIterator错误。可以把这个数据流看做是一个有序的序列,但我们却不能提前直到这个序列的长度,只能不断的调用next()函数事项按需计算下一个数据,所以Iterator的计算是惰性的,只有在需要返回下一个数据的时候才会计算,Iterator甚至可以表示出一个无限大的数据流,例如全体自然数,而使用list是完全不可能存储全体自然数的。

总结:
凡是可以作用于 for 循环的对象都是 Iterable类型。
凡是可以作用于 next()函数的对象都是 Iterator类型,表示一种惰性计算。
集合数据类型如:listdictstr 等是Iterable但不是Iterator,不过可以通过iter()函数获取Iterator对象。